/*
 * Copyright 2007 Robert Hanson <iamroberthanson AT gmail.com>
 * Copyright 2009 Matias Kruk <kruk.matias AT gmail.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hod.client.backend.orm.rebind;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;


public class GearsDataStoreGenerator extends Generator
{

    private static final String TMPL_ROOT = "com/hod/client/backend/orm/rebind/tmpl/";
    private static final String ANNOTATION_NAMED_QUERY = "gears.namedQuery";
    private static final String ANNOTATION_INDEX = "gears.index";
    private static final String ANNOTATION_TABLE = "gears.table";
    private static final String ANNOTATION_COLUMN = "gears.column";
    private static final String ANNOTATION_ID = "gears.id";
    
    public String generate (TreeLogger logger, GeneratorContext context, String typeName)
            throws UnableToCompleteException
    {
        try {
            TypeOracle typeOracle = context.getTypeOracle();
            JClassType requestedClass = typeOracle.getType(typeName);
            
            String packageName = requestedClass.getPackage().getName();
            String beanClassName = requestedClass.getSimpleSourceName();
            String dataStoreClassName = beanClassName + "_DataStore";
            String qualifiedDataStoreClassName = packageName + "." + dataStoreClassName;
            
            SourceWriter writer = getSourceWriter(logger, context, packageName, dataStoreClassName);
            if (writer == null) {
                // the class already exists, sso just return
                return qualifiedDataStoreClassName;
            }
           
            // testing
            Map<String, String> tableProps = getProperties(requestedClass.getMetaData(ANNOTATION_TABLE));
            ArrayList fieldProps = new ArrayList();
            ArrayList keyFields = new ArrayList();
            Map<String, String> namedQueries = getNamedQueries(requestedClass.getMetaData(ANNOTATION_NAMED_QUERY), logger);
            Map<String, String> tableIndexes = getTableIndexes(requestedClass.getMetaData(ANNOTATION_INDEX), logger);

            JField[] fields = requestedClass.getFields();
            for (int j = 0; j < fields.length; j++) {
                FieldProperties fieldProp = getFieldProperties(fields[j], logger);
                if (fieldProp == null) {
                    continue;
                }
                
                fieldProps.add(fieldProp);
                if (fieldProp.isIdentifier()) {
                    keyFields.add(fieldProp);
                }
            }

            JMethod[] methods = requestedClass.getMethods();
            for (int j = 0; j < methods.length; j++) {
                FieldProperties fieldProp = getFieldProperties(methods[j], logger);
                if (fieldProp == null) {
                    continue;
                }
                
                fieldProps.add(fieldProp);
                if (fieldProp.isIdentifier()) {
                    keyFields.add(fieldProp);
                }
            }
            if (namedQueries!=null){
            	//logger.log(TreeLogger.INFO, "Hay algo en NamedQueries", null);
	            fixupQueries(namedQueries, tableProps.get("name"), fieldProps);
	            for (Object key : namedQueries.keySet())
	              {
	                 logger.log(TreeLogger.INFO, (String) namedQueries.get(key), null);
	              }
            }

            VelocityEngine ve = new VelocityEngine();
            ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "class");
            ve.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
            ve.setProperty("velocimacro.library", TMPL_ROOT + "GearsDataMacros.vm");

            try {
                ve.init();
                VelocityContext velocityCtx = new VelocityContext();
                
                velocityCtx.put("dataStoreClassName", dataStoreClassName);
                velocityCtx.put("dataBeanClassName", beanClassName);
                velocityCtx.put("dbTableName", tableProps.get("name"));
                velocityCtx.put("namespace", tableProps.get("namespace"));
                velocityCtx.put("keyFields", keyFields);
                velocityCtx.put("fields", fieldProps);
                if (namedQueries!=null) velocityCtx.put("finders", namedQueries);
                if (tableIndexes != null){
	                velocityCtx.put("indexName",tableIndexes.get("name"));
	                velocityCtx.put("fieldIndexes",tableIndexes.get("fields"));
	                velocityCtx.put("createIndexes","1");
                }
                
                Template template = null;
                template = ve.getTemplate(TMPL_ROOT + "GearsDataStore_subclass.vm");
                StringWriter sw = new StringWriter();
                template.merge(velocityCtx, sw);
                writer.println(sw.toString());
                //logger.log(TreeLogger.INFO,sw.toString(), null);
                
            }
            catch (Exception e) {
                e.printStackTrace();
            }
            writer.commit(logger);
            return qualifiedDataStoreClassName;
        }
        catch (Exception e) {
            e.printStackTrace();
            throw new UnableToCompleteException();
        }

    }


    private void fixupQueries(Map<String, String> namedQueries, String tableName, ArrayList<FieldProperties> fieldNames)
   {
      if (namedQueries != null)
      {
         StringBuffer fields = new StringBuffer();
         for (FieldProperties field : fieldNames)
         {
            if (fields.length() > 0) fields.append(", ");
            fields.append(field.getColumn());
         }
         for (String key : namedQueries.keySet())
         {
            String query = namedQueries.get(key);
            query = query.replace(":" + tableName, fields.toString() + " from " + tableName);
            namedQueries.put(key, query);
         }
      }
   }


   private FieldProperties getFieldProperties (JMethod method, TreeLogger logger) throws Exception
    {
        FieldProperties result = new FieldProperties();
        
        Map idProps = getProperties(method.getMetaData(ANNOTATION_ID));
        Map columnProps = getProperties(method.getMetaData(ANNOTATION_COLUMN));
        Map propsInUse = null;
        
        if (idProps == null && columnProps == null) {
            logger.log(TreeLogger.INFO, method.getName() + ": not persistent", null);
            return null;
        }
        if (!(method.getName().startsWith("get") || method.getName().startsWith("set"))) {
            RuntimeException e = new RuntimeException(method.getName() + ": not a getter or setter");
            logger.log(TreeLogger.INFO, method.getName() + ": not a getter or setter", e);
            return null;
        }

        if (idProps != null) {
            propsInUse = idProps;
            logger.log(TreeLogger.INFO, method.getName() + ": id field", null);
            result.setIdentifier(true);
        }
        else {
            propsInUse = columnProps;
            logger.log(TreeLogger.INFO, method.getName() + ": data column", null);
            result.setIdentifier(false);
        }

        result.setFieldName(method.getName().substring(3));
        result.setGetterName("get" + ucFirst(method.getName().substring(3)));
        result.setSetterName("set" + ucFirst(method.getName().substring(3)));
        result.setColumn((String) (propsInUse.get("name") == null ? method.getName().substring(3) : propsInUse.get("name")));
        result.setPrecision(propsInUse.get("length") == null ? 0 : Integer.parseInt((String) propsInUse.get("length")));
        result.setScale(propsInUse.get("scale") == null ? 0 : Integer.parseInt((String) propsInUse.get("scale")));
        result.setColumnType(propsInUse.get("type") == null ? getColumnType(method) : (String)propsInUse.get("type"));
        
        if (method.getName().startsWith("get")) {
            result.setJavaType(method.getReturnType().getQualifiedSourceName());
        }
        else if (method.getName().startsWith("set")) {
            result.setJavaType(method.getParameters()[0].getType().getQualifiedSourceName());
        }
        result.setForeignKey(propsInUse.get("references") == null ? false: true);
        result.setReferences(propsInUse.get("references") == null ? "": propsInUse.get("references").toString());
        logger.log(TreeLogger.INFO, propsInUse.get("references") + ": references", null);
        return result;
    }


    private FieldProperties getFieldProperties (JField field, TreeLogger logger) throws Exception
    {
        FieldProperties result = new FieldProperties();
        
        Map idProps = getProperties(field.getMetaData(ANNOTATION_ID));
        Map columnProps = getProperties(field.getMetaData(ANNOTATION_COLUMN));
        Map propsInUse = null;
        
        if (idProps == null && columnProps == null) {
            logger.log(TreeLogger.INFO, field.getName() + ": not persistent", null);
            return null;
        }

        if (idProps != null) {
            propsInUse = idProps;
            logger.log(TreeLogger.INFO, field.getName() + ": id field", null);
            result.setIdentifier(true);
        }
        else {
            propsInUse = columnProps;
            logger.log(TreeLogger.INFO, field.getName() + ": data column", null);
            result.setIdentifier(false);
        }

        result.setFieldName(field.getName());
        result.setGetterName("get" + ucFirst(field.getName()));
        result.setSetterName("set" + ucFirst(field.getName()));
        result.setColumn((String) (propsInUse.get("name") == null ? field.getName() : propsInUse.get("name")));
        result.setPrecision(propsInUse.get("length") == null ? 0 : Integer.parseInt((String) propsInUse.get("length")));
        result.setScale(propsInUse.get("scale") == null ? 0 : Integer.parseInt((String) propsInUse.get("scale")));
        result.setColumnType(propsInUse.get("type") == null ? getColumnType(field) : (String)propsInUse.get("type"));
        result.setJavaType(field.getType().getQualifiedSourceName());
        result.setForeignKey(propsInUse.get("references") == null ? false: true);
        result.setReferences(propsInUse.get("references") == null ? "": propsInUse.get("references").toString());
        logger.log(TreeLogger.INFO, propsInUse.get("references") + ": references", null);
        
        return result;
    }


    private String ucFirst (String name)
    {
        return name.substring(0, 1).toUpperCase() + name.substring(1);
    }


    private String getColumnType (JField field)
    {
        String className = field.getType().getQualifiedSourceName();
        String type = determineTypeFromClassName(className);
        if (type == null) {
            System.out.println("TODO: " + className);
        }
        return type;
    }


    private String determineTypeFromClassName (String className)
    {
        if ("java.lang.String".equals(className)) return "varchar";
        else if ("boolean".equals(className)) return "boolean";
        else if ("byte".equals(className)) return "number";
        else if ("char".equals(className)) return "number";
        else if ("short".equals(className)) return "number";
        else if ("integer".equals(className)) return "number";
        else if ("long".equals(className)) return "number";
        else if ("float".equals(className)) return "number";
        else if ("double".equals(className)) return "number";
        else if ("java.lang.Boolean".equals(className)) return "boolean";
        else if ("java.lang.Byte".equals(className)) return "number";
        else if ("java.lang.Char".equals(className)) return "number";
        else if ("java.lang.Short".equals(className)) return "number";
        else if ("java.lang.Integer".equals(className)) return "integer";
        else if ("java.lang.Long".equals(className)) return "number";
        else if ("java.lang.Float".equals(className)) return "number";
        else if ("java.lang.Double".equals(className)) return "number";
        else if ("java.util.Date".equals(className)) return "date";
        return null;
    }

    
    private String getColumnType (JMethod method)
    {
        if (method.getName().startsWith("get")) {
            return determineTypeFromClassName(method.getReturnType().getQualifiedSourceName());
        }
        else if (method.getName().startsWith("set")) {
            return determineTypeFromClassName(method.getParameters()[0].getType().getQualifiedSourceName());
        }
        else {
            return null;
        }
    }


    private Map getProperties (String[][] metaData) throws Exception
    {
        HashMap result = new HashMap();
        
        if (metaData.length == 0) {
            return null;
        }

        String[] metaDataContent = metaData[0];
        String metaDataString = "";
        
        for (int i = 0; i < metaDataContent.length; i++) {
            metaDataString += metaDataContent[i];
        }
        
        if (metaDataString.length() == 0) {
            return result;
        }
        
        if (! metaDataString.matches("\\(.*\\)")) {
            throw new Exception("malformed properties: " + metaDataString);
        }

        metaDataString = metaDataString.substring(1, metaDataString.length() - 1);
        //System.out.println(metaDataString);

        String[] kvStringPairs = metaDataString.split("\\s*,\\s*");
        for (int i = 0; i < kvStringPairs.length; i++) {
            String[] keyVal = kvStringPairs[i].split("\\s*=\\s*");
            if (keyVal.length != 2) {
                throw new Exception("malformed properties: " + metaDataString);
            }
            result.put(keyVal[0], keyVal[1]);
        }
        
        return result;
    }
    
    private Map getTableIndexes(String[][] metaData, TreeLogger logger) throws Exception
    {
        HashMap result = new HashMap();
        
        if (metaData.length == 0) {
            return null;
        }

        String[] metaDataContent = metaData[0];
        String metaDataString = "";
        
        for (int i = 0; i < metaDataContent.length; i++) {
            metaDataString += metaDataContent[i];
        }
        
        if (metaDataString.length() == 0) {
            return result;
        }
        
        if (! metaDataString.matches("\\(.*\\)")) {
            throw new Exception("malformed properties: " + metaDataString);
        }

        metaDataString = metaDataString.substring(1, metaDataString.length() - 1);
        //System.out.println(metaDataString);

        String[] kvStringPairs = metaDataString.split("\\s*,\\s*");
        for (int i = 0; i < kvStringPairs.length; i++) {
            String[] keyVal = kvStringPairs[i].split("\\s*=\\s*");
            if (keyVal.length != 2) {
                throw new Exception("malformed properties: " + metaDataString+"+"+keyVal[0]+keyVal[1]);
            }
            result.put(keyVal[0], keyVal[1].replace("_", ",")); //para cambiar el delimitador de los fieldss
        }
        
        return result;
    }
    
    private Map getNamedQueries(String[][] metadata, TreeLogger logger)
   {
      Map<String, String> namedQueries = null;

      if (metadata != null && metadata.length > 0)
      {
    	  //logger.log(TreeLogger.INFO, "Hay metadata de NamedQueries", null);   
         // first paste the metadata back into continuous Strings
         for (int i = 0; i < metadata.length; i++)
         {
            StringBuffer mds = new StringBuffer();
            for (int j = 0; j < metadata[i].length; j++)
            {
               if (j != 0) mds.append(" ");
               mds.append(metadata[i][j]);
            }
            
            // then break it up in a meaningfull way
            if (!mds.toString().matches("\\(.*\\)"))
            {
               logger.log(TreeLogger.WARN, "Invalid @" + ANNOTATION_NAMED_QUERY + " format: " + mds.toString(), null);
               continue;
            }

            Map<String, String> properties = getQueryParams(mds.toString());

            if (properties.containsKey("name") && properties.containsKey("sql"))
            {
               if (namedQueries == null) namedQueries = new HashMap<String, String>();
               namedQueries.put(properties.get("name"), properties.get("sql"));
            }
            else
            {
               logger.log(TreeLogger.WARN, "Invalid @" + ANNOTATION_NAMED_QUERY + " format: " + mds.toString(), null);
            }
         }
      }

      return namedQueries;
   }

    private Map<String, String> getQueryParams(String metadataString)
    {
       String params = metadataString.substring(1, metadataString.length()-1);
       Map<String, String> properties = new HashMap<String, String>();
       String cc;
       boolean outsideString = true;
       StringBuffer currentWord = new StringBuffer();
       String name = "";
       for (int j = 0; j < params.length(); j++)
       {
          cc = params.substring(j, j+1);
          if("=".equals(cc) && outsideString)
          {
             // name value separator
             name = currentWord.toString();
             currentWord = new StringBuffer();
          }
          else if(",".equals(cc) && outsideString)
          {
             // property separator
             properties.put(name, currentWord.toString());
             currentWord = new StringBuffer();
             name = "";
          }
          else if ("\"".equals(cc))
          {
             int l = currentWord.length();
             if (l > 0)
             {
                if (!"\\".equals(currentWord.substring(l - 1)))
                   outsideString = !outsideString;
                else
                {
                   // unescape quotes
                   if (!outsideString) currentWord.replace(l - 1, l, cc);
                }
             }
             else
                outsideString = !outsideString;
          }
          else if(" ".equals(cc) && outsideString)
          {
             // ignore spaces
          }
          else
          {
             currentWord.append(cc);
          }
       }
       // save our last property
       properties.put(name, currentWord.toString());
       return properties;
    }
    
    private SourceWriter getSourceWriter (TreeLogger logger, GeneratorContext context, String packageName, String dataStoreClassName) throws UnableToCompleteException
    {
        PrintWriter printWriter = context.tryCreate(logger, packageName, dataStoreClassName);
        if (printWriter == null) {
            //throw new UnableToCompleteException();
            // should be ok, the file already exists
            return null;
        }
        
        ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(packageName, dataStoreClassName);
        composerFactory.setSuperclass("com.hod.client.backend.orm.client.GearsDataStore");
        
        composerFactory.addImport("java.util.ArrayList");
        composerFactory.addImport("java.util.Date");
        composerFactory.addImport("java.util.HashMap");
        composerFactory.addImport("java.util.Map");
        composerFactory.addImport("com.google.gwt.core.client.JavaScriptObject");
        composerFactory.addImport("com.google.gwt.gears.client.GearsException");
        composerFactory.addImport("com.google.gwt.gears.client.database.Database");
        composerFactory.addImport("com.google.gwt.user.client.Window");
        composerFactory.addImport("com.hod.client.backend.database.DBUtil");
        composerFactory.addImport("com.google.gwt.gears.client.database.ResultSet");
        composerFactory.addImport("com.google.gwt.gears.client.database.DatabaseException");
        composerFactory.addImport("com.hod.client.backend.orm.client.GearsDataStore");
        composerFactory.addImport("com.hod.client.backend.orm.client.GearsQuery");
        composerFactory.addImport("com.google.gwt.i18n.client.DateTimeFormat");

        SourceWriter writer = composerFactory.createSourceWriter(context, printWriter);
        return writer;
    }
}
